# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2023 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#    * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
#      may be used to endorse or promote products derived from this software
#      without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import datetime
import codecs
import typing
import collections.abc
import logging


from django.db import transaction
from uds.models.cache import Cache as DBCache
from uds.core.util.model import sql_now
from uds.core.util import serializer
from uds.core import consts

from .hash import hash_key

logger = logging.getLogger(__name__)


class Cache:
    # Simple hits vs missses counters
    hits = 0
    misses = 0

    _owner: str
    _timeout: int

    @staticmethod
    def _basic_serialize(value: typing.Any) -> str:
        return codecs.encode(serializer.serialize(value), 'base64').decode()

    @staticmethod
    def _basic_deserialize(value: str) -> typing.Any:
        return serializer.deserialize(codecs.decode(value.encode(), 'base64'))

    _serializer: typing.ClassVar[collections.abc.Callable[[typing.Any], str]] = _basic_serialize
    _deserializer: typing.ClassVar[collections.abc.Callable[[str], typing.Any]] = _basic_deserialize

    def __init__(self, owner: typing.Union[str, bytes], default_timeout: int = consts.cache.DEFAULT_CACHE_TIMEOUT) -> None:
        self._owner = owner.decode('utf-8') if isinstance(owner, bytes) else owner
        self._timeout = default_timeout

    def _get_key(self, key: typing.Union[str, bytes]) -> str:
        if isinstance(key, str):
            key = key.encode('utf8')
        return hash_key(self._owner.encode() + key)

    def get(
        self, skey: typing.Union[str, bytes], default: typing.Any = None, *, remove: bool = False
    ) -> typing.Any:
        now = sql_now()
        # logger.debug('Requesting key "%s" for cache "%s"', skey, self._owner)
        try:
            key = self._get_key(skey)
            # logger.debug('Key: %s', key)
            c: DBCache = DBCache.objects.get(owner=self._owner, pk=key)
            # If expired
            if now > c.created + datetime.timedelta(seconds=c.validity):
                return default

            try:
                # logger.debug('value: %s', c.value)
                val = Cache._deserializer(c.value)
            except Exception:  # If invalid, simple do not use it
                # logger.exception('Invalid deserialization value from cache. Removing it.')
                c.delete()
                return default

            Cache.hits += 1
            # If we are asked to remove it, do it now
            if remove:
                c.delete()
            return val
        except DBCache.DoesNotExist:  # @UndefinedVariable
            Cache.misses += 1
            # logger.debug('key not found: %s', skey)
            return default
        # except OperationalError:
        # If database is not ready, just return default value
        # This is not a big issue, since cache is not critical
        # and probably will be generated by sqlite on high concurrency

        #    Cache.misses += 1
        #    return defValue
        except Exception:
            import inspect

            # Get caller
            error = 'Error Getting cache key from: '
            for caller in inspect.stack():
                error += f'{caller.filename}:{caller.lineno} -> '
            logger.error(error)

            # logger.exception('Error getting cache key: %s', skey)
            Cache.misses += 1
            return default

    def pop(self, skey: typing.Union[str, bytes], default: typing.Any = None) -> typing.Any:
        """
        Removes an stored cached item and returns it or default if not found
        If cached item does not exists, just returns default, but else, it will be removed
        """
        return self.get(skey, default=default, remove=True)

    def __getitem__(self, key: typing.Union[str, bytes]) -> typing.Any:
        """
        Returns the cached value for the given key using the [] operator
        """
        return self.get(key)

    def remove(self, skey: typing.Union[str, bytes]) -> bool:
        """
        Removes an stored cached item
        If cached item does not exists, nothing happens (no exception thrown)
        """
        # logger.debug('Removing key "%s" for uService "%s"' % (skey, self._owner))
        try:
            key = self._get_key(skey)
            DBCache.objects.get(pk=key).delete()  # @UndefinedVariable
            return True
        except DBCache.DoesNotExist:  # @UndefinedVariable
            logger.debug('key not found')
            return False

    def __delitem__(self, key: typing.Union[str, bytes]) -> None:
        """
        Removes an stored cached item using the [] operator
        """
        self.remove(key)

    def clear(self) -> None:
        with transaction.atomic():
            Cache.delete(self._owner)

    def put(
        self,
        skey: typing.Union[str, bytes],
        value: typing.Any,
        validity: typing.Optional[int] = None,
    ) -> None:
        # logger.debug('Saving key "%s" for cache "%s"' % (skey, self._owner,))
        validity = validity if validity is not None else self._timeout
        key = self._get_key(skey)
        value_str = Cache._serializer(value)
        now = sql_now()
        # Remove existing if any and create a new one
        with transaction.atomic():
            try:
                DBCache.objects.update_or_create(
                    pk=key,
                    owner=self._owner,
                    defaults={
                        'owner': self._owner,
                        'key': key,
                        'value': value_str,
                        'created': now,
                        'validity': validity,
                    },
                )

                # # Remove if existing
                # DBCache.objects.filter(pk=key).delete()
                # # And create a new one
                # DBCache.objects.create(
                #     owner=self._owner,
                #     key=key,
                #     value=strValue,
                #     created=now,
                #     validity=validity,
                # )  # @UndefinedVariable
                return  # And return
            except Exception as e:
                logger.debug('Transaction in course, cannot store value: %s', e)

    def __setitem__(self, key: typing.Union[str, bytes], value: typing.Any) -> None:
        """
        Stores a value in the cache using the [] operator with default validity
        """
        self.put(key, value)

    def refresh(self, skey: typing.Union[str, bytes]) -> None:
        # logger.debug('Refreshing key "%s" for cache "%s"' % (skey, self._owner,))
        try:
            key = self._get_key(skey)
            c = DBCache.objects.get(pk=key)
            c.created = sql_now()
            c.save()
        except DBCache.DoesNotExist:
            logger.debug('Can\'t refresh cache key %s because it doesn\'t exists', skey)
            return

    @staticmethod
    def purge() -> None:
        with transaction.atomic():
            DBCache.objects.all().delete()

    @staticmethod
    def purge_outdated() -> None:
        # purge_outdated has a transaction.atomic() inside
        DBCache.purge_outdated()

    @staticmethod
    def delete(owner: typing.Optional[str] = None) -> None:
        with transaction.atomic():
            # logger.info("Deleting cache items")
            if owner is None:
                objects = DBCache.objects.all()
            else:
                objects = DBCache.objects.filter(owner=owner)
            objects.delete()
